/**
 * Copyright 2020 - 2023 the original author or Linlan authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package net.linlan.commons.core;

import net.linlan.commons.core.abs.StringConstants;

import java.io.*;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.text.MessageFormat;
import java.util.*;
import java.util.Map.Entry;

/**
 * String class to provide utils for use.
 * This class contains utility methods for string operations such as checking if a string is blank,
 * starting with a certain prefix, and trimming whitespace. These utils are intended for use in commons group packages.
 *
 * @author Your Name (you can replace this with the actual author's name)
 * @version 1.0
 * @since 1.0
 */
public final class StringUtils extends StringConstants {
    /**
     * Represents a failed index search.
     */
    public static final int INDEX_NOT_FOUND = -1;

    /**
     * Private constructor to prevent instantiation of this utility class.
     */
    private StringUtils() {
    }

    /**
     * Checks if a given CharSequence is blank. A CharSequence is considered blank if it is null,
     * has a length of 0, or consists only of whitespace characters.
     *
     * @param source the source CharSequence to check
     * @return true if the source is blank, false otherwise
     */
    public static boolean isBlank(final CharSequence source) {
        int length;

        if ((source == null) || ((length = source.length()) == 0)) {
            return true;
        }

        if (source.equals(NULL)){
            return true;
        }
        for (int i = 0; i < length; i++) {
            // if has any char then not null
            if (false == NumberUtils.isBlankChar(source.charAt(i))) {
                return false;
            }
        }

        return true;
    }

    /**
     * Checks if a given CharSequence is not blank. A CharSequence is considered not blank if it
     * is not null, has a non-zero length, and contains at least one non-whitespace character.
     *
     * @param source the source CharSequence to check
     * @return true if the source is not blank, false otherwise
     */
    public static boolean isNotBlank(final CharSequence source) {
        return false == isBlank(source);
    }

    /**
     * Checks if any of the given CharSequences are blank.
     *
     * @param source an array of CharSequences to check
     * @return true if any of the CharSequences are blank, false otherwise
     */
    public static boolean hasBlank(final CharSequence... source) {
        if (ArrayUtils.isEmpty(source)) {
            return true;
        }

        for (CharSequence str : source) {
            if (isBlank(str)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Checks if a given CharSequence is empty. A CharSequence is considered empty if it is null,
     * has a length of 0, or consists only of whitespace characters or the string "null".
     *
     * @param source the source CharSequence to check
     * @return true if the source is empty, false otherwise
     */
    public static boolean isEmpty(final CharSequence source) {
        return null==source || "".equals(source.toString().trim())
                || "null".equalsIgnoreCase(source+"") || source.length() == 0;
    }

    /**
     * Checks if a given CharSequence is not empty. A CharSequence is considered not empty if it
     * is not null, has a non-zero length, and contains at least one non-whitespace character.
     *
     * @param source the source CharSequence to check
     * @return true if the source is not empty, false otherwise
     */
    public static boolean isNotEmpty(final CharSequence source) {
        return false == isEmpty(source);
    }

    /**
     * Converts a null CharSequence to an empty string.
     *
     * @param source the source CharSequence to convert
     * @return the source CharSequence as a String, or an empty string if the source is null
     */
    public static String nullToEmpty(final CharSequence source) {
        return nullToDefault(source, EMPTY);
    }

    /**
     * Converts an empty CharSequence to null.
     *
     * @param source the source CharSequence to convert
     * @return the source CharSequence as a String, or null if the source is empty
     */
    public static String emptyToNull(final CharSequence source) {
        return isEmpty(source) ? null : source.toString();
    }

    /**
     * Returns the default string if the source is null, otherwise returns the source string.
     *

     * nullToDefault(null, "default")  = "default"
     * nullToDefault("", "default")    = ""
     * nullToDefault("  ", "default")  = "  "
     * nullToDefault("bat", "default") = "bat"

     *
     * @param source the source CharSequence to check
     * @param defStr the default string to return if the source is null
     * @return the source string if it is not null, otherwise the default string
     */
    public static String nullToDefault(final CharSequence source, final String defStr) {
        return (source == null || source == "") ? defStr : source.toString();
    }

    /**
     * Checks if any of the given CharSequences are empty.
     *
     * @param source an array of CharSequences to check
     * @return true if any of the CharSequences are empty, false otherwise
     */
    public static boolean hasEmpty(final CharSequence... source) {
        if (ArrayUtils.isEmpty(source)) {
            return true;
        }

        for (CharSequence str : source) {
            if (isEmpty(str)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Check that the given {@code CharSequence} is neither {@code null} nor
     * of length 0.
     * Note: this method returns {@code true} for a {@code CharSequence}
     * that purely consists of whitespace.
     * StringUtils.hasLength(null) = false
     * StringUtils.hasLength("") = false
     * StringUtils.hasLength(" ") = true
     * StringUtils.hasLength("Hello") = true

     *
     * @param source the {@code CharSequence} to check (may be {@code null})
     * @return {@code true} if the {@code CharSequence} is not {@code null} and has length
     * @see StringUtils#hasText(String)
     */
    public static boolean hasLength(CharSequence source) {
        return (source != null && source.length() > 0);
    }

    /**
     * Check that the given {@code String} is neither {@code null} nor
     * of length 0.
     * <p>Note: this method returns {@code true} for a {@code String}
     * that purely consists of whitespace.
     * StringUtils.hasLength(null) = false
     * StringUtils.hasLength("") = false
     * StringUtils.hasLength(" ") = true
     * StringUtils.hasLength("Hello") = true

     *
     * @param source the {@code String} to check (may be {@code null})
     * @return {@code true} if the {@code String} is not {@code null} and has length
     * @see StringUtils#hasText(String)
     */
    public static boolean hasLength(String source) {
        return hasLength((CharSequence) source);
    }

    /**
     * Check whether the given {@code CharSequence} contains actual <em>text</em>.
     * <p>More specifically, this method returns {@code true} if the
     * {@code CharSequence} is not {@code null}, its length is greater than
     * 0, and it contains at least one non-whitespace character.
     * StringUtils.hasText(null) = false
     * StringUtils.hasText("") = false
     * StringUtils.hasText(" ") = false
     * StringUtils.hasText("12345") = true
     * StringUtils.hasText(" 12345 ") = true

     *
     * @param str the {@code CharSequence} to check (may be {@code null})
     * @return {@code true} if the {@code CharSequence} is not {@code null},
     * its length is greater than 0, and it does not contain whitespace only
     * @see Character#isWhitespace
     */
    public static boolean hasText(CharSequence str) {
        if (!hasLength(str)) {
            return false;
        }
        int strLen = str.length();
        for (int i = 0; i < strLen; i++) {
            if (!Character.isWhitespace(str.charAt(i))) {
                return true;
            }
        }
        return false;
    }

    /**
     * Check whether the given {@code String} contains actual <em>text</em>.
     * <p>More specifically, this method returns {@code true} if the
     * {@code String} is not {@code null}, its length is greater than
     * 0, and it contains at least one non-whitespace character.
     * StringUtils.hasText(null) = false
     * StringUtils.hasText("") = false
     * StringUtils.hasText(" ") = false
     * StringUtils.hasText("12345") = true
     * StringUtils.hasText(" 12345 ") = true

     *
     * @param source the {@code String} to check (may be {@code null})
     * @return {@code true} if the {@code String} is not {@code null},
     * its length is greater than 0, and it does not contain whitespace only
     * @see Character#isWhitespace
     */
    public static boolean hasText(String source) {
        return hasText((CharSequence) source);
    }

    /**
     * Trims the whitespace from the beginning and end of a CharSequence.
     * If the source is null, returns null.
     * <p>
     * This trim method is different from <code>String.trim</code> as it uses <code>NumberUtils.isBlankChar</code>
     * to handle whitespace, which can trim English, Chinese, and other blank characters.
     *

     * trim(null)          = null
     * trim("")            = ""
     * trim("     ")       = ""
     * trim("abc")         = "abc"
     * trim("    abc    ") = "abc"

     *
     * @param source the source CharSequence to trim
     * @return the trimmed String, or null if the source is null
     */
    public static String trim(final CharSequence source) {
        return (null == source) ? null : trim(source, 0);
    }

    /**
     * Trims the whitespace from the beginning and end of each String in an array.
     *
     * @param source the array of Strings to trim
     */
    public static void trim(final String[] source) {
        if (null == source) {
            return;
        }
        String str;
        for (int i = 0; i < source.length; i++) {
            str = source[i];
            if (null != str) {
                source[i] = str.trim();
            }
        }
    }

    /**
     * Trims the whitespace from the beginning and/or end of a CharSequence based on the mode.
     * If the source is null, returns null.
     * <p>
     * This trim method is different from <code>String.trim</code> as it uses <code>NumberUtils.isBlankChar</code>
     * to handle whitespace, which can trim English, Chinese, and other blank characters.
     *
     * @param source the source CharSequence to trim
     * @param mode <code>-1</code> to trim the beginning, <code>0</code> to trim both ends, <code>1</code> to trim the end
     * @return the trimmed String, or null if the source is null
     */
    public static String trim(final CharSequence source, final int mode) {
        if (source == null) {
            return null;
        }

        int length = source.length();
        int start = 0;
        int end = length;

        // trim the head
        if (mode <= 0) {
            while ((start < end) && (NumberUtils.isBlankChar(source.charAt(start)))) {
                start++;
            }
        }

        // trim the bottom
        if (mode >= 0) {
            while ((start < end) && (NumberUtils.isBlankChar(source.charAt(end - 1)))) {
                end--;
            }
        }

        if ((start > 0) || (end < length)) {
            return source.toString().substring(start, end);
        }

        return source.toString();
    }

    /**
     * <p>Removes control characters (char &lt;= 32) from both
     * ends of this String returning an empty String ("") if the String
     * is empty ("") after the trim or if it is {@code null}.
     *
     * <p>The String is trimmed using {@link String#trim()}.
     * Trim removes start and end characters &lt;= 32.
     * To strip whitespace use {@link #stripToEmpty(String)}.</p>
     *

     * StringUtils.trimToEmpty(null)          = ""
     * StringUtils.trimToEmpty("")            = ""
     * StringUtils.trimToEmpty("     ")       = ""
     * StringUtils.trimToEmpty("abc")         = "abc"
     * StringUtils.trimToEmpty("    abc    ") = "abc"

     *
     * @param str  the String to be trimmed, may be null
     * @return the trimmed String, or an empty String if {@code null} input
     * @since 2.0
     */
    public static String trimToEmpty(final String str) {
        return str == null ? EMPTY : str.trim();
    }

    /**
     * if the source string is start with prefix
     *
     * @param source source string
     * @param prefix prefix
     * @param isIgnoreCase if true, ignore case lower or upper
     * @return if start with true, else false
     */
    public static boolean startsWith(final CharSequence source, final CharSequence prefix, final boolean isIgnoreCase) {
        if (source == null || prefix == null) {
            return source == prefix;
        }
        if (prefix.length() > source.length()) {
            return false;
        }
        return regionMatches(source, isIgnoreCase, 0, prefix, 0, prefix.length());
    }

    /**
     * if the source string is start with prefix, the default is to ignore case
     *
     * @param source source string
     * @param prefix prefix
     * @return if start with true, else false
     */
    public static boolean startsWith(final CharSequence source, final CharSequence prefix) {
        return startsWith(source, prefix, false);
    }

    /**
     * if the source string is start with prefixes, more prefixes input, the default is to ignore case
     *
     * @param source source string
     * @param prefixes prefixes, more prefixes input
     * @return if start with true, else false
     */
    public static boolean startsWith(final CharSequence source, final CharSequence... prefixes) {
        if (isEmpty(source) || ArrayUtils.isEmpty(prefixes)) {
            return false;
        }

        for (CharSequence suffix : prefixes) {
            if (startsWith(source, suffix, false)) {
                return true;
            }
        }
        return false;
    }

    public static boolean startsWithIgnoreCase(final CharSequence str, final CharSequence prefix) {
        return startsWith(str, prefix, true);
    }

    public static boolean startsWithAny(final CharSequence sequence, final CharSequence... searchStrings) {
        if (isEmpty(sequence) || ArrayUtils.isEmpty(searchStrings)) {
            return false;
        }
        for (final CharSequence searchString : searchStrings) {
            if (startsWith(sequence, searchString)) {
                return true;
            }
        }
        return false;
    }

    public static boolean endsWithIgnoreCase(final CharSequence str, final CharSequence prefix) {
        return endsWith(str, prefix, true);
    }

    /**
     * if the source string is end with suffix
     *
     * @param source source string
     * @param suffix suffix
     * @param isIgnoreCase if true, ignore case lower or upper
     * @return if end with true, else false
     */
    public static boolean endsWith(final CharSequence source, final CharSequence suffix, final boolean isIgnoreCase) {
        if (isBlank(source) || isBlank(suffix)) {
            return false;
        }

        if (isIgnoreCase) {
            return source.toString().toLowerCase().endsWith(suffix.toString().toLowerCase());
        } else {
            return source.toString().endsWith(suffix.toString());
        }
    }

    /**
     * if the source string is end with suffix, the default is to ignore case
     *
     * @param source source string
     * @param suffix suffix
     * @return if end with true, else false
     */
    public static boolean endsWith(final CharSequence source, final CharSequence suffix) {
        return endsWith(source, suffix, false);
    }


    /**
     * if the source string is end with suffix, suffixes input, the default is to ignore case
     *
     * @param source source string
     * @param suffixes suffixes, suffixes input
     * @return if end with true, else false
     */
    public static boolean endsWith(final CharSequence source, final CharSequence... suffixes) {
        if (isEmpty(source) || ArrayUtils.isEmpty(suffixes)) {
            return false;
        }

        for (CharSequence suffix : suffixes) {
            if (endsWith(source, suffix, false)) {
                return true;
            }
        }
        return false;
    }

    /**
     * if the source string is contain with dest string
     *
     * @param source source string
     * @param destStr dest string
     * @param isIgnoreCase if true, ignore case lower or upper
     * @return if contain true, else false
     */
    public static boolean contains(final CharSequence source, final CharSequence destStr, final boolean isIgnoreCase) {
        if (null == source) {
            return null == destStr;
        }
        if (isIgnoreCase) {
            return source.toString().toLowerCase().contains(destStr.toString().toLowerCase());
        }else{
            return source.toString().contains(destStr.toString());
        }
    }

    /**
     * if the source string is contain the dest string, destStrs input, the default is to ignore case
     *
     * @param source source string
     * @param destStrs destStr, destStrs input
     * @return if contain true, else false
     */
    public static boolean contains(final CharSequence source, final CharSequence... destStrs) {
        if (isEmpty(source) || ArrayUtils.isEmpty(destStrs)) {
            return false;
        }

        for (CharSequence destStr : destStrs) {
            if (contains(source, destStr, false)) {
                return true;
            }
        }
        return false;
    }


    /**
     * remove source string when find from string to default ""<br>
     * such as:remove("ab-cd-ef-gh", "-"):abcdefgh,
     * the default replace to {@link StringUtils#EMPTY}
     *
     * @param source source string
     * @param remove remove string
     * @return {@link String} the string of source input
     */
    public static String remove(final String source, final CharSequence remove) {
        return replace(source, remove, EMPTY);
    }

    /**
     * remove source string when find from char to default ""<br>
     * such as:remove("ab-cd-ef-gh", "-"):abcdefgh,
     * the default replace to {@link StringUtils#EMPTY}
     *
     * @param source source string
     * @param remove remove string
     * @return {@link String} the string of source input
     */
    public static String remove(final String source, final char remove) {
        if (isEmpty(source) || source.indexOf(remove) == INDEX_NOT_FOUND) {
            return source;
        }
        final char[] chars = source.toCharArray();
        int pos = 0;
        for (int i = 0; i < chars.length; i++) {
            if (chars[i] != remove) {
                chars[pos++] = chars[i];
            }
        }
        return new String(chars, 0, pos);
    }

    /**
     * replace source string from from string to to string <br>
     *
     * @param source source string
     * @param from from string
     * @param to to string
     * @return {@link String} the string of source input
     */
    public static String replace(final String source, final CharSequence from, final CharSequence to) {
        return source.replace(from, to);
    }


    /**
     * compare two string to sort, null is less, the default is ignore case
     *
     * @param str1 string1
     * @param str2 string2
     * @param nullIsLess {@code null} is in front, null is less
     * @return sort result, -1:str1 &lt; str2, 1:str1 &gt; str2, 0:str1 == str2
     */
    public static int compare(final CharSequence str1, final CharSequence str2, final boolean nullIsLess) {
        return compareIgnoreCase(str1, str2, nullIsLess, true);
    }

    /**
     * compare two string to sort, null is less
     *

     * compareIgnoreCase(null, null, *)     = 0
     * compareIgnoreCase(null , "a", true)  &lt; 0
     * compareIgnoreCase(null , "a", false) &gt; 0
     * compareIgnoreCase("a", null, true)   &gt; 0
     * compareIgnoreCase("a", null, false)  &lt; 0
     * compareIgnoreCase("abc", "abc", *)   = 0
     * compareIgnoreCase("abc", "ABC", *)   = 0
     * compareIgnoreCase("a", "b", *)       &lt; 0
     * compareIgnoreCase("b", "a", *)       &gt; 0
     * compareIgnoreCase("a", "B", *)       &lt; 0
     * compareIgnoreCase("A", "b", *)       &lt; 0
     * compareIgnoreCase("ab", "abc", *)    &lt; 0

     *
     * @param str1 string1
     * @param str2 string2
     * @param nullIsLess {@code null} is in front, null is less
     * @param isIgnoreCase if ignore case
     * @return sort result, -1:str1 &lt; str2, 1:str1 &gt; str2, 0:str1 == str2
     */
    public static int compareIgnoreCase(final CharSequence str1, final CharSequence str2, final boolean nullIsLess, final boolean isIgnoreCase) {
        if (str1 == str2) {
            return 0;
        }
        if (str1 == null) {
            return nullIsLess ? -1 : 1;
        }
        if (str2 == null) {
            return nullIsLess ? 1 : -1;
        }
        if (isIgnoreCase) {
            return str1.toString().compareToIgnoreCase(str2.toString());
        }else{
            return str1.toString().compareTo(str2.toString());
        }
    }

    /**
     * split source string by sep char<br>
     * such as:a,b,c,d:split(source, ','), return [a,b,c,d] <br>
     * a,b,,d:split(source, ','), return [a,b,'',d] <br>
     *
     * @param source source string
     * @param spt separator
     * @return List String
     */
    public static List<String> split(final CharSequence source, final char spt) {
        return split(source, spt, 0);
    }

    /**
     * split source string by sep char<br>
     * such as:a,b,c,d:split(source, ","), return [a,b,c,d] <br>
     * a,b,,d:split(source, ","), return [a,b,"",d] <br>
     *
     * @param source source string
     * @param spt separator
     * @param limit limit
     * @return List String
     */
    public static List<String> split(final CharSequence source, final char spt, final int limit) {
        if (source == null) {
            return null;
        }
        List<String> list = new ArrayList<>(limit > 0 ? MAX_SPLIT_LIMIT : limit);
        if (limit == 1) {
            list.add(source.toString());
            return list;
        }
        // if split to be end
        boolean isNotEnd = true;
        int strLen = source.length();
        StringBuilder sb = new StringBuilder(strLen);
        char c;
        for (int i = 0; i < strLen; i++) {
            c = source.charAt(i);
            if (isNotEnd && c == spt) {
                list.add(sb.toString());
                // clean StringBuilder
                sb.delete(0, sb.length());

                // if reach limit, leave all the next to be one
                if (limit > 0 && list.size() == limit - 1) {
                    isNotEnd = false;
                }
            } else {
                sb.append(c);
            }
        }
        // add the bottom
        list.add(sb.toString());
        return list;
    }

    /**
     * split source string by separator string<br>
     * such as:a,b,c,d:split(source, ","), return {a,b,c,d} <br>
     * a,b,,d:split(source, ","), return {a,b,"",d} <br>
     *
     * @param source source string
     * @param spt separator
     * @return {@link String} the string of source input[]
     */
    public static String[] split(final CharSequence source, final CharSequence spt) {
        if (source == null) {
            return null;
        }

        final String str = source.toString();
        if (str.trim().length() == 0) {
            return new String[] { str };
        }

        int len = spt.length(); // delete length
        int max = (source.length() / len) + 2; // one more for the last
        int[] positions = new int[max];

        int i, j = 0;
        int count = 0;
        positions[0] = -len;
        final String delimiter2 = spt.toString();
        while ((i = str.indexOf(delimiter2, j)) != -1) {
            count++;
            positions[count] = i;
            j = i + len;
        }
        count++;
        positions[count] = source.length();

        String[] result = new String[count];

        for (i = 0; i < count; i++) {
            result[i] = str.substring(positions[i] + len, positions[i + 1]);
        }
        return result;
    }

    /**
     * split source string by separator string and trim space<br>
     * such as:a,b,c,d:split(source, ","), return {a,b,c,d} <br>
     * a,b,,d:split(source, ","), return {a,b,d} <br>
     *
     * @param source source string
     * @param spt separator
     * @return {@link String} the string of source input[]
     */
    public static String[] splitAndTrim(final CharSequence source, final CharSequence spt) {
        String[] array = split(source, spt);
        for (int i = 0, len = array.length; i < len; i++) {
            array[i] = array[i].trim();
        }
        return array;
    }


    /**
     * Splits the {@code delimited} string (delimited by the specified {@code separator} character) and returns the
     * delimited values as a {@code Set}.
     * If either argument is {@code null}, this method returns {@code null}.
     *
     * @param delimited the string to split
     * @param separator the character that delineates individual tokens to split
     * @return the delimited values as a {@code Set}.
     * @since 1.2
     */
    public static Set<String> splitToSet(String delimited, String separator) {
        if (delimited == null || separator == null) {
            return null;
        }
        String[] split = splitAndTrim(delimited, separator);
        return CollectionUtils.asSet(split);
    }

    /**
     * repeat char to make up equals length or other
     *
     * @param c the repeat char
     * @param count the count of repeat, if count &lt; equals 0, return ""
     * @return {@link String} the string of source input
     */
    public static String repeat(final char c, final int count) {
        if (count <= 0) {
            return EMPTY;
        }

        char[] result = new char[count];
        for (int i = 0; i < count; i++) {
            result[i] = c;
        }
        return new String(result);
    }

    /**
     * repeat string to make up equals length or other
     *
     * @param source the repeat string
     * @param count the count of repeat, if count &lt; equals 0, return ""
     * @return {@link String} the string of source input
     */
    public static String repeat(final CharSequence source, final int count) {
        if (null == source) {
            return null;
        }
        if (count <= 0) {
            return EMPTY;
        }
        if (count == 1 || source.length() == 0) {
            return source.toString();
        }

        // check
        final int len = source.length();
        final long longSize = (long) len * (long) count;
        final int size = (int) longSize;
        if (size != longSize) {
            throw new ArrayIndexOutOfBoundsException("Required String length is too large: " + longSize);
        }

        final char[] array = new char[size];
        source.toString().getChars(0, len, array, 0);
        int n;
        // n <<= 1 means n *2
        for (n = len; n < size - n; n <<= 1) {
            System.arraycopy(array, 0, array, n, n);
        }
        System.arraycopy(array, 0, array, n, size - n);
        return new String(array);
    }

    // Equals
    //-----------------------------------------------------------------------
    /**
     * Compares two CharSequences, returning {@code true} if they represent
     * equal sequences of characters.
     *
     * {@code null}s are handled without exceptions. Two {@code null}
     * references are considered to be equal. The comparison is <strong>case sensitive</strong>.
     *

     * StringUtils.equals(null, null)   = true
     * StringUtils.equals(null, "abc")  = false
     * StringUtils.equals("abc", null)  = false
     * StringUtils.equals("abc", "abc") = true
     * StringUtils.equals("abc", "ABC") = false

     *
     * @param cs1  the first CharSequence, may be {@code null}
     * @param cs2  the second CharSequence, may be {@code null}
     * @return {@code true} if the CharSequences are equal (case-sensitive), or both {@code null}
     * @since 3.0 Changed signature from equals(String, String) to equals(CharSequence, CharSequence)
     * @see Object#equals(Object)
     * @see #equalsIgnoreCase(CharSequence, CharSequence)
     */
    public static boolean equals(final CharSequence cs1, final CharSequence cs2) {
        if (cs1 == cs2) {
            return true;
        }
        if (cs1 == null || cs2 == null) {
            return false;
        }
        if (cs1.length() != cs2.length()) {
            return false;
        }
        if (cs1 instanceof String && cs2 instanceof String) {
            return cs1.equals(cs2);
        }
        // Step-wise comparison
        final int length = cs1.length();
        for (int i = 0; i < length; i++) {
            if (cs1.charAt(i) != cs2.charAt(i)) {
                return false;
            }
        }
        return true;
    }

    /**
     * <p>Compares given {@code string} to a CharSequences vararg of {@code searchStrings},
     * returning {@code true} if the {@code string} is equal to any of the {@code searchStrings}.</p>
     *

     * StringUtils.equalsAny(null, (CharSequence[]) null) = false
     * StringUtils.equalsAny(null, null, null)    = true
     * StringUtils.equalsAny(null, "abc", "def")  = false
     * StringUtils.equalsAny("abc", null, "def")  = false
     * StringUtils.equalsAny("abc", "abc", "def") = true
     * StringUtils.equalsAny("abc", "ABC", "DEF") = false

     *
     * @param string to compare, may be {@code null}.
     * @param searchStrings a vararg of strings, may be {@code null}.
     * @return {@code true} if the string is equal (case-sensitive) to any other element of {@code searchStrings};
     * {@code false} if {@code searchStrings} is null or contains no matches.
     * @since 3.5
     */
    public static boolean equalsAny(final CharSequence string, final CharSequence... searchStrings) {
        if (ArrayUtils.isNotEmpty(searchStrings)) {
            for (final CharSequence next : searchStrings) {
                if (equals(string, next)) {
                    return true;
                }
            }
        }
        return false;
    }
    /**
     * the two string if equals, ignore case
     *

     * equals(null, null)   = true
     * equals(null, &quot;abcde&quot;)  = false
     * equals(&quot;abcde&quot;, null)  = false
     * equals(&quot;abcde&quot;, &quot;abcde&quot;) = true
     * equals(&quot;abcde&quot;, &quot;ABCDE&quot;) = false

     *
     * @param str1 string1
     * @param str2 string2
     *
     * @return if equals, then true
     */
    public static boolean equalsIgnoreCase(final CharSequence str1, final CharSequence str2) {
        if (str1 == null) {
            return str2 == null;
        }

        return str1.toString().equalsIgnoreCase(str2.toString());
    }

    /**
     * format the source string with template, {} is placeholders<br>
     * format method can replace {} to parameter in order<br>
     * if need to output {}, can use \\
     * if need to output \, can use \\\\ <br>
     * such as:<br>
     * format("this is {} for {}", "abc", "xyz"), the result is:this is abc for xyz<br>
     * format("this is \\{} for {}", "abc", "xyz"), the result is:this is \{} for abc<br>
     * format("this is \\\\{} for {}", "abc", "xyz"), the result is:this is \abc for xyz<br>
     *
     * @param pattern pattern to use to replace parameter in {}
     * @param params params
     * @return {@link String} the string of source input
     */
    public static String format(final CharSequence pattern, final Object... params) {
        if (null == pattern) {
            return null;
        }
        if (ArrayUtils.isEmpty(params) || isBlank(pattern)) {
            return pattern.toString();
        }
        return format(pattern.toString(), params);
    }

    /**
     * format the source string with pattern, {} is placeholders<br>
     * format method can replace {} to parameter in order<br>
     * if need to output {}, can use \\
     * if need to output \, can use \\\\ <br>
     * such as:<br>
     * format("this is {} for {}", "abc", "xyz"), the result is:this is abc for xyz<br>
     * format("this is \\{} for {}", "abc", "xyz"), the result is:this is \{} for abc<br>
     * format("this is \\\\{} for {}", "abc", "xyz"), the result is:this is \abc for xyz<br>
     *
     * @param pattern string pattern in {}
     * @param params params
     * @return {@link String} the string of source input
     */
    private static String format(final String pattern, final Object... params) {
        if (isBlank(pattern) || ArrayUtils.isEmpty(params)) {
            return pattern;
        }
        final int length = pattern.length();

        //to init sb long length to get more performance
        StringBuilder sb = new StringBuilder(length + 50);

        //the handled position
        int handledPosition = 0;
        //the index place of holder
        int holderIndex;
        for (int argIndex = 0; argIndex < params.length; argIndex++) {
            holderIndex = pattern.indexOf(EMPTY_JSON, handledPosition);
            //if the next part have not placeholders
            if (holderIndex == -1) {
                //return pattern without placeholders
                if (handledPosition == 0) {
                    return pattern;
                } else {
                    //if the next part of pattern have not placeholders, return the next part
                    sb.append(pattern, handledPosition, length);
                    return sb.toString();
                }
            } else {
                //deal with the \\
                if (holderIndex > 0 && pattern.charAt(holderIndex - 1) == C_BACKSLASH) {
                    //deal with the \\\\
                    if (holderIndex > 1 && pattern.charAt(holderIndex - 2) == C_BACKSLASH) {
                        //if there is \\ before \\, the placeholders need to be deal
                        sb.append(pattern, handledPosition, holderIndex - 1);
                        sb.append(utf8String(params[argIndex]));
                        handledPosition = holderIndex + 2;
                    } else {
                        //deal with the placeholders
                        argIndex--;
                        sb.append(pattern, handledPosition, holderIndex - 1);
                        sb.append(C_DELIM_START);
                        handledPosition = holderIndex + 1;
                    }
                } else {
                    //deal with the placeholders without any \\ or \\\\
                    sb.append(pattern, handledPosition, holderIndex);
                    sb.append(utf8String(params[argIndex]));
                    handledPosition = holderIndex + 2;
                }
            }
        }
        // append the characters following the last {} pair.
        sb.append(pattern, handledPosition, pattern.length());

        return sb.toString();
    }

    /**
     * through index in {} to format string, use {number} as placeholders<br>
     * such as:<br>
     * format("this is {0} for {1}", "abc", "xyz"), the result is:this is abc for xyz<br>
     *
     * @param pattern pattern of string
     * @param params params
     * @return {@link String} the string of source input
     */
    public static String formatOfIndex(final CharSequence pattern, final Object... params) {
        return MessageFormat.format(pattern.toString(), params);
    }

    /**
     * the source string with pattern use map K and V, {varName} is placeholders<br>
     * map = {abc: "abcValue", xyz: "xyzValue"}
     * format("{abc} and {xyz}", map), the result is:abcValue and xyzValue
     *
     * @param pattern pattern of string {key}
     * @param map the map of value
     * @return {@link String} the string of source input
     */
    public static String format(final CharSequence pattern, final Map<?, ?> map) {
        if (null == pattern) {
            return null;
        }
        if (null == map || map.isEmpty()) {
            return pattern.toString();
        }

        final String result = pattern.toString();
        for (Entry<?, ?> entry : map.entrySet()) {
            result.replace("{" + entry.getKey() + "}", utf8String(entry.getValue()));
        }
        return result;
    }

    /**
     * get the byte of source string with UTF-8 Charset, the default charset is UTF-8
     *
     * @param source source string
     * @return byte[]
     */
    public static byte[] utf8Bytes(final CharSequence source) {
        return getBytes(source, CharsetUtils.CHARSET_UTF_8);
    }


    /**
     * get the byte of source sting with input charset
     *
     * @param source source string
     * @param charset Charset, if null use os
     * @return byte[]
     */
    public static byte[] getBytes(final CharSequence source, final Charset charset) {
        if (source == null) {
            return null;
        }

        if (null == charset) {
            return source.toString().getBytes();
        }
        return source.toString().getBytes(charset);
    }

    /**
     * get the string of source object with UTF-8 Charset, the default charset is UTF-8
     *
     * @param source source object
     * @return {@link String} the string of source input
     */
    public static String utf8String(final Object source) {
        return getString(source, CharsetUtils.CHARSET_UTF_8);
    }

    /**
     * get the string of source object with input charset
     * byte[] and ByteBuffer will get the String array
     * Object array will use Arrays.toString
     *
     * @param source source object
     * @param charset input charset
     * @return {@link String} the string of source input
     */
    public static String getString(final Object source, final Charset charset) {
        if (null == source) {
            return null;
        }

        if (source instanceof String) {
            return (String) source;
        } else if (source instanceof byte[]) {
            return getString((byte[]) source, charset);
        } else if (ArrayUtils.isArray(source)) {
            return ArrayUtils.toString(source);
        }

        return source.toString();
    }


    /**
     * get the string of source byte[] with input charset
     *
     * @param source source byte[]
     * @param charset Charset, if null use os
     * @return {@link String} the string of source input
     */
    public static String getString(final byte[] source, final Charset charset) {
        if (source == null) {
            return null;
        }

        if (null == charset) {
            return new String(source);
        }
        return new String(source, charset);
    }


    /**
     * get the string of source ByteBuffer with input charset
     *
     * @param source source ByteBuffer
     * @param charset Charset, if null use os
     * @return {@link String} the string of source input
     */
    public static String getString(ByteBuffer source, Charset charset) {
        if (null == charset) {
            charset = Charset.defaultCharset();
        }
        return charset.decode(source).toString();
    }


    /**
     * new builder of StringBuilder
     *
     * @return {@link String} the string of source inputBuilder
     */
    public static StringBuilder builder() {
        return new StringBuilder();
    }

    /**
     * new builder of StringBuilder with capacity
     *
     * @param capacity init capacity
     * @return {@link String} the string of source inputBuilder
     */
    public static StringBuilder builder(int capacity) {
        return new StringBuilder(capacity);
    }

    /**
     * new builder of StringBuilder
     *
     * @param source init builder string
     * @return {@link String} the string of source inputBuilder
     */
    public static StringBuilder builder(CharSequence... source) {
        final StringBuilder sb = new StringBuilder();
        for (CharSequence str : source) {
            sb.append(str);
        }
        return sb;
    }

    /**
     * get reader of StringReader
     *
     * @param source source string
     * @return {@link String} the string of source inputReader
     */
    public static StringReader getReader(CharSequence source) {
        if (null == source) {
            return null;
        }
        return new StringReader(source.toString());
    }

    /**
     * get writer of StringWriter
     *
     * @return {@link String} the string of source inputWriter
     */
    public static StringWriter getWriter() {
        return new StringWriter();
    }

    /**
     * write to Writer provide to direct method<br>
     *
     * @param source a string
     * @param writer {@link Writer}
     * @return {@link Writer}
     * @throws IOException IO exception
     */
    public static Writer writeByWriter(String source, Writer writer)
            throws IOException {
        if (isEmpty(source)) {
            writer.write("\"\"");
            return writer;
        }

        char b;        //back char
        char c = 0; //current char
        String s;
        int i;
        int len = source.length();

        writer.write('"');
        for (i = 0; i < len; i++) {
            b = c;
            c = source.charAt(i);
            switch (c) {
                case '\\':
                case '"':
                    writer.write('\\');
                    writer.write(c);
                    break;
                case '/':
                    if (b == '<') {
                        writer.write('\\');
                    }
                    writer.write(c);
                    break;
                case '\b':
                    writer.write("\\b");
                    break;
                case '\t':
                    writer.write("\\t");
                    break;
                case '\n':
                    writer.write("\\n");
                    break;
                case '\f':
                    writer.write("\\f");
                    break;
                case '\r':
                    writer.write("\\r");
                    break;
                default:
                    if (c < ' ' || (c >= '\u0080' && c < '\u00a0') || (c >= '\u2000' && c < '\u2100')) {
                        writer.write("\\u");
                        s = Integer.toHexString(c);
                        writer.write("0000", 0, 4 - s.length());
                        writer.write(s);
                    } else {
                        writer.write(c);
                    }
            }
        }
        writer.write('"');
        return writer;
    }

    /**
     * count the dest char in source string<br>
     *
     * @param source 内容
     * @param findChar char to be found
     * @return count of found
     */
    public static int count(final CharSequence source, final char findChar) {
        int count = 0;
        if (isEmpty(source)) {
            return 0;
        }
        int contentLength = source.length();
        for (int i = 0; i < contentLength; i++) {
            if (findChar == source.charAt(i)) {
                count++;
            }
        }
        return count;
    }

    /**
     * count the dest string in source string<br>
     * if source is {@code null} or "", return {@code 0}
     *

     * count(null, *)       = 0
     * count("", *)         = 0
     * count("abccba", null)  = 0
     * count("abccba", "")    = 0
     * count("abccba", "a")   = 2
     * count("abccba", "ab")  = 1
     * count("abccba", "xxx") = 0

     *
     * @param source source string
     * @param findStr string to be found
     * @return count of found
     */
    public static int count(final CharSequence source, final CharSequence findStr) {
        if (hasEmpty(source, findStr) || findStr.length() > source.length()) {
            return 0;
        }

        int count = 0;
        int idx = 0;
        final String content2 = source.toString();
        final String strForSearch2 = findStr.toString();
        while ((idx = content2.indexOf(strForSearch2, idx)) > -1) {
            count++;
            idx += findStr.length();
        }
        return count;
    }


    /**
     * 将字符串切分为N等份
     *
     * @param str 字符串
     * @param partLength 每等份的长度
     * @return 切分后的数组
     * @since 3.0.6
     */
    public static String[] cut(final CharSequence str, final int partLength) {
        if (null == str) {
            return null;
        }
        int len = str.length();
        if (len < partLength) {
            return new String[] { str.toString() };
        }
        int part = NumberUtils.count(len, partLength);
        final String[] array = new String[part];

        final String str2 = str.toString();
        for (int i = 0; i < part; i++) {
            array[i] = str2.substring(i * partLength, (i == part - 1) ? len : (partLength + i * partLength));
        }
        return array;
    }

    /**
     * implicit the source with ... if the too long
     * such as:"xxx...xxx"
     *
     * @param source source string
     * @param length the length of implicit after
     * @return {@link String} the string of source input
     */
    public static String implicit(final CharSequence source, final int length) {
        if (null == source) {
            return null;
        }
        if ((source.length() + 3) <= length) {
            return source.toString();
        }
        int w = length / 2;
        int l = source.length();

        final String str2 = source.toString();
        return format("{}...{}", str2.substring(0, length - w), str2.substring(l - w));
    }


    /**
     * index of dest char of the source string
     *
     * @param source source string
     * @param dest the string to be found
     * @return the first position
     */
    public static int indexOf(final CharSequence source, final char dest) {
        return indexOf(source, dest, 0);
    }

    /**
     * index of dest char of the source string from the pos
     *
     * @param source source string
     * @param dest the string to be found
     * @param begin the position to begin
     * @return the first position
     */
    public static int indexOf(final CharSequence source, final char dest, final int begin) {
        if (source instanceof String) {
            return ((String) source).indexOf(dest, begin);
        } else {
            return indexOf(source, dest, begin, -1);
        }
    }

    /**
     * index of dest char of the source string between the begin to end
     *
     * @param source source string
     * @param dest the string to be found
     * @param begin the position to begin
     * @param end the position to end
     * @return the first position
     */
    public static int indexOf(final CharSequence source, final char dest, int begin, int end) {
        int len = source.length();
        if (begin < 0 || begin > len) {
            begin = 0;
        }
        if (end > len || end < 0) {
            end = len;
        }
        for (int i = begin; i < end; i++) {
            if (source.charAt(i) == dest) {
                return i;
            }
        }
        return -1;
    }

    /**
     * last index of dest char of the source string
     *
     * @param source source string
     * @param dest the string to be found
     * @return the first position
     */
    public static int lastIndexOf(final CharSequence source, final char dest) {
        return ((String) source).lastIndexOf(dest);
    }


    /**
     * 以指定的分隔符来进行字符串元素连接，例如有字符串数组array和连接符为逗号(,)
     * @param array  需要连接的对象数组
     * @param sep    元素连接之间的分隔符
     * @return 连接好的新字符串
     */
    public static String join(Object[] array, String sep) {
        return join(array, sep, null);
    }

    /**
     * 以指定的分隔符来进行字符串元素连接，例如有字符串数组array和连接符为逗号(,)
     * @param list  需要连接的对象数组
     * @param sep    元素连接之间的分隔符
     * @return 连接好的新字符串
     */
    public static String join(Collection list, String sep) {
        return join(list, sep, null);
    }

    /**
     * 以指定的分隔符来进行字符串元素连接，例如有字符串数组array和连接符为逗号(,)
     * @param list  需要连接的对象数组
     * @param sep    元素连接之间的分隔符
     * @param prefix 前缀字符串
     * @return 连接好的新字符串
     */
    public static String join(Collection list, String sep, String prefix) {
        Object[] array = list == null ? null : list.toArray();
        return join(array, sep, prefix);
    }

    /**
     * 以指定的分隔符来进行字符串元素连接，例如有字符串数组array和连接符为逗号(,)
     * @param array  需要连接的对象数组
     * @param sep    元素连接之间的分隔符
     * @param prefix 前缀字符串
     * @return 连接好的新字符串
     */
    public static String join(Object[] array, String sep, String prefix) {
        if (array == null) {
            return "";
        }

        int arraySize = array.length;

        if (arraySize == 0) {
            return "";
        }

        if (sep == null) {
            sep = "";
        }

        if (prefix == null) {
            prefix = "";
        }

        StringBuilder buf = new StringBuilder(prefix);
        for (int i = 0; i < arraySize; i++) {
            if (i > 0) {
                buf.append(sep);
            }
            buf.append(array[i] == null ? "" : array[i]);
        }
        return buf.toString();
    }

    /**
     * 以指定的分隔符来进行字符串元素连接，例如有字符串数组array和连接符为逗号(,)
     * @param spt    元素连接之间的分隔符
     * @param source  需要连接的对象数组
     * @return 连接好的新字符串
     */
    public static String join(final CharSequence spt, final Object... source) {
        return ArrayUtils.join(source, spt);
    }

    /**
     * 以json元素的方式连接字符串中元素
     * 例如有字符串数组array
     * String[] array = new String[] { "hello", "world", "linlan", "cloud","storage" };
     * 那么得到的结果是:
     * "hello","world","linlan","cloud","storage"
     * @param array 需要连接的字符串数组
     * @return 以json元素方式连接好的新字符串
     */
    public static String jsonJoin(String[] array) {
        int arraySize = array.length;
        int bufSize = arraySize * (array[0].length() + 3);
        StringBuilder buf = new StringBuilder(bufSize);
        for (int i = 0; i < arraySize; i++) {
            if (i > 0) {
                buf.append(',');
            }

            buf.append('"');
            buf.append(array[i]);
            buf.append('"');
        }
        return buf.toString();
    }

    /**
     * remove the prefix of source
     *
     * @param source source string
     * @param prefix prefix
     * @return {@link String} the string of source input
     */
    public static String removePrefix(CharSequence source, CharSequence prefix) {
        return removePrefix(source, prefix, false);
    }

    /**
     * remove the prefix of source
     *
     * @param source source string
     * @param prefix prefix
     * @param isIgnoreCase isIgnoreCase true to ignore case
     * @return {@link String} the string of source input
     */
    public static String removePrefix(CharSequence source, CharSequence prefix, boolean isIgnoreCase) {
        if (isEmpty(source) || isEmpty(prefix)) {
            return source.toString();
        }

        final String dest = source.toString();
        if (startsWith(dest, prefix, isIgnoreCase)) {
            //if prefix match, remove prefix
            return dest.substring(prefix.length());
        }
        //the whole source
        return dest;
    }

    /**
     * get the subString of source string from fromIndex to toIndex<br>
     * index is begin from 0, the last char is -1<br>
     * if the fromIndex == toIndex, return "" <br>
     * if fromIndex or toIndex &lt; 0, use the length of source string
     * @see StringUtils#substring
     * substring("abcdefghijk", 2, 3): c <br>
     * substring("abcdefghijk", 2, -3): cdefghi <br>
     *
     * @param source source string
     * @param fromIndex from index, include from index
     * @return String the substring of fromIndex to toIndex
     */
    public static String substring(CharSequence source, int fromIndex) {
        int len = source.length();
        return substring(source, fromIndex, len);
    }
    /**
     * get the subString of source string from fromIndex to toIndex<br>
     * index is begin from 0, the last char is -1<br>
     * if the fromIndex == toIndex, return "" <br>
     * if fromIndex or toIndex &lt; 0, use the length of source string
     * @see StringUtils#substring
     * substring("abcdefghijk", 2, 3): c <br>
     * substring("abcdefghijk", 2, -3): cdefghi <br>
     *
     * @param source source string
     * @param fromIndex from index, include from index
     * @param toIndex to index, exclude to index
     * @return String the substring of fromIndex to toIndex
     */
    public static String substring(CharSequence source, int fromIndex, int toIndex) {
        int len = source.length();
        if (fromIndex < 0) {
            fromIndex = len + fromIndex;
            if (fromIndex < 0) {
                fromIndex = 0;
            }
        } else if (fromIndex > len) {
            fromIndex = len;
        }

        if (toIndex < 0) {
            toIndex = len + toIndex;
            if (toIndex < 0) {
                toIndex = len;
            }
        } else if (toIndex > len) {
            toIndex = len;
        }

        if (toIndex < fromIndex) {
            int tmp = fromIndex;
            fromIndex = toIndex;
            toIndex = tmp;
        }

        if (fromIndex == toIndex) {
            return StringUtils.EMPTY;
        }

        return source.toString().substring(fromIndex, toIndex);
    }

    /** 将空对象输出为""
     * @param source the source object
     * @return if null then ""
     */
    public static String clearNull(Object source) {
        if (source == null)
            return "";
        if (!(source instanceof String))
            return "";
        return (String) source;
    }

    /** 是否为string数组
     * @param source the input source string
     * @param array the array
     * @return  true
     *          false
     */
    public static boolean inStringArray(String source, String[] array) {
        for (String x : array) {
            if (x.equals(source)) {
                return true;
            }
        }
        return false;
    }

    /**Object类型转String类型
     * @param source 输入对象
     * @return string 对象
     */
    public static String toString(Object source) {
        try {
            if (source == null) {
                return "";
            } else {
                return source.toString();
            }
        } catch (Exception e) {
            return "";
        }
    }


    /**
     * 大写首字母<br>
     * 例如：str = name, return Name
     *
     * @param str 字符串
     * @return 字符串
     */
    public static String upperFirst(CharSequence str) {
        if (null == str) {
            return null;
        }
        if (str.length() > 0) {
            char firstChar = str.charAt(0);
            if (Character.isLowerCase(firstChar)) {
                return Character.toUpperCase(firstChar) + substring(str, 1);
            }
        }
        return str.toString();
    }

    /**
     * 小写首字母
     * 例如：str = Name, return name
     *
     * @param str 字符串
     * @return 字符串
     */
    public static String lowerFirst(CharSequence str) {
        if (null == str) {
            return null;
        }
        if (str.length() > 0) {
            char firstChar = str.charAt(0);
            if (Character.isUpperCase(firstChar)) {
                return Character.toLowerCase(firstChar) + substring(str, 1);
            }
        }
        return str.toString();
    }

    /**
     *
     * @param str 字符串
     * @param delimiters 分割符
     * @param trimTokens 字符
     * @param ignoreEmptyTokens 是否忽略空字符
     * @return 字符串流
     */
    public static String[] tokenizeToStringArray(
            String str, String delimiters, boolean trimTokens, boolean ignoreEmptyTokens) {

        if (str == null) {
            return null;
        }

        StringTokenizer st = new StringTokenizer(str, delimiters);
        List<String> tokens = new ArrayList<String>();
        while (st.hasMoreTokens()) {
            String token = st.nextToken();
            if (trimTokens) {
                token = token.trim();
            }
            if (!ignoreEmptyTokens || token.length() > 0) {
                tokens.add(token);
            }
        }
        return toStringArray(tokens);
    }

    /**
     *
     * @param collection 字符集合
     * @return 字符串流
     */
    public static String[] toStringArray(Collection<String> collection) {
        if (collection == null) {
            return null;
        }
        return collection.toArray(new String[collection.size()]);
    }


    /**
     * Gets a CharSequence length or {@code 0} if the CharSequence is
     * {@code null}.
     *
     * @param cs
     *            a CharSequence or {@code null}
     * @return CharSequence length or {@code 0} if the CharSequence is
     *         {@code null}.
     * @since 2.4
     * @since 3.0 Changed signature from length(String) to length(CharSequence)
     */
    public static int length(final CharSequence cs) {
        return cs == null ? 0 : cs.length();
    }

    /**
     * <p>Checks if CharSequence contains a search CharSequence irrespective of case,
     * handling {@code null}. Case-insensitivity is defined as by
     * {@link String#equalsIgnoreCase(String)}.
     *
     * <p>A {@code null} CharSequence will return {@code false}.</p>
     *

     * StringUtils.containsIgnoreCase(null, *) = false
     * StringUtils.containsIgnoreCase(*, null) = false
     * StringUtils.containsIgnoreCase("", "") = true
     * StringUtils.containsIgnoreCase("abc", "") = true
     * StringUtils.containsIgnoreCase("abc", "a") = true
     * StringUtils.containsIgnoreCase("abc", "z") = false
     * StringUtils.containsIgnoreCase("abc", "A") = true
     * StringUtils.containsIgnoreCase("abc", "Z") = false

     *
     * @param str  the CharSequence to check, may be null
     * @param searchStr  the CharSequence to find, may be null
     * @return true if the CharSequence contains the search CharSequence irrespective of
     * case or false if not or {@code null} string input
     * @since 3.0 Changed signature from containsIgnoreCase(String, String) to containsIgnoreCase(CharSequence, CharSequence)
     */
    public static boolean containsIgnoreCase(final CharSequence str, final CharSequence searchStr) {
        if (str == null || searchStr == null) {
            return false;
        }
        final int len = searchStr.length();
        final int max = str.length() - len;
        for (int i = 0; i <= max; i++) {
            if (regionMatches(str, true, i, searchStr, 0, len)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Green implementation of regionMatches.
     *
     * @param cs the {@code CharSequence} to be processed
     * @param ignoreCase whether or not to be case insensitive
     * @param thisStart the index to start on the {@code cs} CharSequence
     * @param substring the {@code CharSequence} to be looked for
     * @param start the index to start on the {@code substring} CharSequence
     * @param length character length of the region
     * @return whether the region matched
     */
    static boolean regionMatches(final CharSequence cs, final boolean ignoreCase, final int thisStart,
                                 final CharSequence substring, final int start, final int length)    {
        if (cs instanceof String && substring instanceof String) {
            return ((String) cs).regionMatches(ignoreCase, thisStart, (String) substring, start, length);
        }
        int index1 = thisStart;
        int index2 = start;
        int tmpLen = length;

        // Extract these first so we detect NPEs the same as the java.lang.String version
        final int srcLen = cs.length() - thisStart;
        final int otherLen = substring.length() - start;

        // Check for invalid parameters
        if (thisStart < 0 || start < 0 || length < 0) {
            return false;
        }

        // Check that the regions are long enough
        if (srcLen < length || otherLen < length) {
            return false;
        }

        while (tmpLen-- > 0) {
            final char c1 = cs.charAt(index1++);
            final char c2 = substring.charAt(index2++);

            if (c1 == c2) {
                continue;
            }

            if (!ignoreCase) {
                return false;
            }

            // The same check as in String.regionMatches():
            if (Character.toUpperCase(c1) != Character.toUpperCase(c2)
                    && Character.toLowerCase(c1) != Character.toLowerCase(c2)) {
                return false;
            }
        }

        return true;
    }

    // startsWith
    //-----------------------------------------------------------------------

    /**
     * <p>Gets the substring after the last occurrence of a separator.
     * The separator is not returned.</p>
     *
     * <p>A {@code null} string input will return {@code null}.
     * An empty ("") string input will return the empty string.
     * An empty or {@code null} separator will return the empty string if
     * the input string is not {@code null}.</p>
     *
     * <p>If nothing is found, the empty string is returned.</p>
     *

     * StringUtils.substringAfterLast(null, *)      = null
     * StringUtils.substringAfterLast("", *)        = ""
     * StringUtils.substringAfterLast(*, "")        = ""
     * StringUtils.substringAfterLast(*, null)      = ""
     * StringUtils.substringAfterLast("abc", "a")   = "bc"
     * StringUtils.substringAfterLast("abcba", "b") = "a"
     * StringUtils.substringAfterLast("abc", "c")   = ""
     * StringUtils.substringAfterLast("a", "a")     = ""
     * StringUtils.substringAfterLast("a", "z")     = ""

     *
     * @param str  the String to get a substring from, may be null
     * @param separator  the String to search for, may be null
     * @return the substring after the last occurrence of the separator,
     *  {@code null} if null String input
     * @since 2.0
     */
    public static String substringAfterLast(final String str, final String separator) {
        if (isEmpty(str)) {
            return str;
        }
        if (isEmpty(separator)) {
            return EMPTY;
        }
        final int pos = str.lastIndexOf(separator);
        if (pos == INDEX_NOT_FOUND || pos == str.length() - separator.length()) {
            return EMPTY;
        }
        return str.substring(pos + separator.length());
    }

    // SubStringAfter/SubStringBefore
    //-----------------------------------------------------------------------
    /**
     * <p>Gets the substring before the first occurrence of a separator.
     * The separator is not returned.</p>
     *
     * <p>A {@code null} string input will return {@code null}.
     * An empty ("") string input will return the empty string.
     * A {@code null} separator will return the input string.</p>
     *
     * <p>If nothing is found, the string input is returned.</p>
     *

     * StringUtils.substringBefore(null, *)      = null
     * StringUtils.substringBefore("", *)        = ""
     * StringUtils.substringBefore("abc", "a")   = ""
     * StringUtils.substringBefore("abcba", "b") = "a"
     * StringUtils.substringBefore("abc", "c")   = "ab"
     * StringUtils.substringBefore("abc", "d")   = "abc"
     * StringUtils.substringBefore("abc", "")    = ""
     * StringUtils.substringBefore("abc", null)  = "abc"

     *
     * @param str  the String to get a substring from, may be null
     * @param separator  the String to search for, may be null
     * @return the substring before the first occurrence of the separator,
     *  {@code null} if null String input
     * @since 2.0
     */
    public static String substringBefore(final String str, final String separator) {
        if (isEmpty(str) || separator == null) {
            return str;
        }
        if (separator.isEmpty()) {
            return EMPTY;
        }
        final int pos = str.indexOf(separator);
        if (pos == INDEX_NOT_FOUND) {
            return str;
        }
        return str.substring(0, pos);
    }

    /**
     * <p>Gets the substring before the last occurrence of a separator.
     * The separator is not returned.</p>
     *
     * <p>A {@code null} string input will return {@code null}.
     * An empty ("") string input will return the empty string.
     * An empty or {@code null} separator will return the input string.</p>
     *
     * <p>If nothing is found, the string input is returned.</p>
     *

     * StringUtils.substringBeforeLast(null, *)      = null
     * StringUtils.substringBeforeLast("", *)        = ""
     * StringUtils.substringBeforeLast("abcba", "b") = "abc"
     * StringUtils.substringBeforeLast("abc", "c")   = "ab"
     * StringUtils.substringBeforeLast("a", "a")     = ""
     * StringUtils.substringBeforeLast("a", "z")     = "a"
     * StringUtils.substringBeforeLast("a", null)    = "a"
     * StringUtils.substringBeforeLast("a", "")      = "a"

     *
     * @param str  the String to get a substring from, may be null
     * @param separator  the String to search for, may be null
     * @return the substring before the last occurrence of the separator,
     *  {@code null} if null String input
     * @since 2.0
     */
    public static String substringBeforeLast(final String str, final String separator) {
        if (isEmpty(str) || isEmpty(separator)) {
            return str;
        }
        final int pos = str.lastIndexOf(separator);
        if (pos == INDEX_NOT_FOUND) {
            return str;
        }
        return str.substring(0, pos);
    }

    // Substring between
    //-----------------------------------------------------------------------
    /**
     * <p>Gets the String that is nested in between two instances of the
     * same String.</p>
     *
     * <p>A {@code null} input String returns {@code null}.
     * A {@code null} tag returns {@code null}.</p>
     *

     * StringUtils.substringBetween(null, *)            = null
     * StringUtils.substringBetween("", "")             = ""
     * StringUtils.substringBetween("", "tag")          = null
     * StringUtils.substringBetween("tagabctag", null)  = null
     * StringUtils.substringBetween("tagabctag", "")    = ""
     * StringUtils.substringBetween("tagabctag", "tag") = "abc"

     *
     * @param str  the String containing the substring, may be null
     * @param tag  the String before and after the substring, may be null
     * @return the substring, {@code null} if no match
     * @since 2.0
     */
    public static String substringBetween(final String str, final String tag) {
        return substringBetween(str, tag, tag);
    }

    // endsWith
    //-----------------------------------------------------------------------

    /**
     * <p>Gets the String that is nested in between two Strings.
     * Only the first match is returned.</p>
     *
     * <p>A {@code null} input String returns {@code null}.
     * A {@code null} open/close returns {@code null} (no match).
     * An empty ("") open and close returns an empty string.</p>
     *

     * StringUtils.substringBetween("wx[b]yz", "[", "]") = "b"
     * StringUtils.substringBetween(null, *, *)          = null
     * StringUtils.substringBetween(*, null, *)          = null
     * StringUtils.substringBetween(*, *, null)          = null
     * StringUtils.substringBetween("", "", "")          = ""
     * StringUtils.substringBetween("", "", "]")         = null
     * StringUtils.substringBetween("", "[", "]")        = null
     * StringUtils.substringBetween("yabcz", "", "")     = ""
     * StringUtils.substringBetween("yabcz", "y", "z")   = "abc"
     * StringUtils.substringBetween("yabczyabcz", "y", "z")   = "abc"

     *
     * @param str  the String containing the substring, may be null
     * @param open  the String before the substring, may be null
     * @param close  the String after the substring, may be null
     * @return the substring, {@code null} if no match
     * @since 2.0
     */
    public static String substringBetween(final String str, final String open, final String close) {
        if (str == null || open == null || close == null) {
            return null;
        }
        final int start = str.indexOf(open);
        if (start != INDEX_NOT_FOUND) {
            final int end = str.indexOf(close, start + open.length());
            if (end != INDEX_NOT_FOUND) {
                return str.substring(start + open.length(), end);
            }
        }
        return null;
    }

    /**
     * <p>Searches a String for substrings delimited by a start and end tag,
     * returning all matching substrings in an array.</p>
     *
     * <p>A {@code null} input String returns {@code null}.
     * A {@code null} open/close returns {@code null} (no match).
     * An empty ("") open/close returns {@code null} (no match).</p>
     *

     * StringUtils.substringsBetween("[a][b][c]", "[", "]") = ["a","b","c"]
     * StringUtils.substringsBetween(null, *, *)            = null
     * StringUtils.substringsBetween(*, null, *)            = null
     * StringUtils.substringsBetween(*, *, null)            = null
     * StringUtils.substringsBetween("", "[", "]")          = []

     *
     * @param str  the String containing the substrings, null returns null, empty returns empty
     * @param open  the String identifying the start of the substring, empty returns null
     * @param close  the String identifying the end of the substring, empty returns null
     * @return a String Array of substrings, or {@code null} if no match
     * @since 2.3
     */
    public static String[] substringsBetween(final String str, final String open, final String close) {
        if (str == null || isEmpty(open) || isEmpty(close)) {
            return null;
        }
        final int strLen = str.length();
        if (strLen == 0) {
            return ArrayUtils.EMPTY_STRING_ARRAY;
        }
        final int closeLen = close.length();
        final int openLen = open.length();
        final List<String> list = new ArrayList<>();
        int pos = 0;
        while (pos < strLen - closeLen) {
            int start = str.indexOf(open, pos);
            if (start < 0) {
                break;
            }
            start += openLen;
            final int end = str.indexOf(close, start);
            if (end < 0) {
                break;
            }
            list.add(str.substring(start, end));
            pos = end + closeLen;
        }
        if (list.isEmpty()) {
            return null;
        }
        return list.toArray(ArrayUtils.EMPTY_STRING_ARRAY);
    }

    /**
     * <p>Strips whitespace from the start and end of a String  returning
     * an empty String if {@code null} input.</p>
     *

     * StringUtils.stripToEmpty(null)     = ""
     * StringUtils.stripToEmpty("")       = ""
     * StringUtils.stripToEmpty("   ")    = ""
     * StringUtils.stripToEmpty("abc")    = "abc"
     * StringUtils.stripToEmpty("  abc")  = "abc"
     * StringUtils.stripToEmpty("abc  ")  = "abc"
     * StringUtils.stripToEmpty(" abc ")  = "abc"
     * StringUtils.stripToEmpty(" ab c ") = "ab c"

     *
     * @param str  the String to be stripped, may be null
     * @return the trimmed String, or an empty String if {@code null} input
     * @since 2.0
     */
    public static String stripToEmpty(final String str) {
        return str == null ? EMPTY : strip(str, null);
    }

    // Stripping
    //-----------------------------------------------------------------------
    /**
     * <p>Strips whitespace from the start and end of a String.</p>
     *
     * <p>A {@code null} input String returns {@code null}.</p>
     *

     * StringUtils.strip(null)     = null
     * StringUtils.strip("")       = ""
     * StringUtils.strip("   ")    = ""
     * StringUtils.strip("abc")    = "abc"
     * StringUtils.strip("  abc")  = "abc"
     * StringUtils.strip("abc  ")  = "abc"
     * StringUtils.strip(" abc ")  = "abc"
     * StringUtils.strip(" ab c ") = "ab c"

     *
     * @param str  the String to remove whitespace from, may be null
     * @return the stripped String, {@code null} if null String input
     */
    public static String strip(final String str) {
        return strip(str, null);
    }

    /**
     * <p>Strips any of a set of characters from the start and end of a String.
     * This is similar to {@link String#trim()} but allows the characters
     * to be stripped to be controlled.</p>
     *
     * <p>A {@code null} input String returns {@code null}.
     * An empty string ("") input returns the empty string.</p>
     *
     * <p>If the stripChars String is {@code null}, whitespace is
     * stripped as defined by {@link Character#isWhitespace(char)}.
     * Alternatively use {@link #strip(String)}.</p>
     *

     * StringUtils.strip(null, *)          = null
     * StringUtils.strip("", *)            = ""
     * StringUtils.strip("abc", null)      = "abc"
     * StringUtils.strip("  abc", null)    = "abc"
     * StringUtils.strip("abc  ", null)    = "abc"
     * StringUtils.strip(" abc ", null)    = "abc"
     * StringUtils.strip("  abcyx", "xyz") = "  abc"

     *
     * @param str  the String to remove characters from, may be null
     * @param stripChars  the characters to remove, null treated as whitespace
     * @return the stripped String, {@code null} if null String input
     */
    public static String strip(String str, final String stripChars) {
        if (isEmpty(str)) {
            return str;
        }
        str = stripStart(str, stripChars);
        return stripEnd(str, stripChars);
    }

    /**
     * <p>Strips any of a set of characters from the start of a String.</p>
     *
     * <p>A {@code null} input String returns {@code null}.
     * An empty string ("") input returns the empty string.</p>
     *
     * <p>If the stripChars String is {@code null}, whitespace is
     * stripped as defined by {@link Character#isWhitespace(char)}.</p>
     *

     * StringUtils.stripStart(null, *)          = null
     * StringUtils.stripStart("", *)            = ""
     * StringUtils.stripStart("abc", "")        = "abc"
     * StringUtils.stripStart("abc", null)      = "abc"
     * StringUtils.stripStart("  abc", null)    = "abc"
     * StringUtils.stripStart("abc  ", null)    = "abc  "
     * StringUtils.stripStart(" abc ", null)    = "abc "
     * StringUtils.stripStart("yxabc  ", "xyz") = "abc  "

     *
     * @param str  the String to remove characters from, may be null
     * @param stripChars  the characters to remove, null treated as whitespace
     * @return the stripped String, {@code null} if null String input
     */
    public static String stripStart(final String str, final String stripChars) {
        final int strLen = length(str);
        if (strLen == 0) {
            return str;
        }
        int start = 0;
        if (stripChars == null) {
            while (start != strLen && Character.isWhitespace(str.charAt(start))) {
                start++;
            }
        } else if (stripChars.isEmpty()) {
            return str;
        } else {
            while (start != strLen && stripChars.indexOf(str.charAt(start)) != INDEX_NOT_FOUND) {
                start++;
            }
        }
        return str.substring(start);
    }

    /**
     * <p>Strips any of a set of characters from the end of a String.</p>
     *
     * <p>A {@code null} input String returns {@code null}.
     * An empty string ("") input returns the empty string.</p>
     *
     * <p>If the stripChars String is {@code null}, whitespace is
     * stripped as defined by {@link Character#isWhitespace(char)}.</p>
     *

     * StringUtils.stripEnd(null, *)          = null
     * StringUtils.stripEnd("", *)            = ""
     * StringUtils.stripEnd("abc", "")        = "abc"
     * StringUtils.stripEnd("abc", null)      = "abc"
     * StringUtils.stripEnd("  abc", null)    = "  abc"
     * StringUtils.stripEnd("abc  ", null)    = "abc"
     * StringUtils.stripEnd(" abc ", null)    = " abc"
     * StringUtils.stripEnd("  abcyx", "xyz") = "  abc"
     * StringUtils.stripEnd("120.00", ".0")   = "12"

     *
     * @param str  the String to remove characters from, may be null
     * @param stripChars  the set of characters to remove, null treated as whitespace
     * @return the stripped String, {@code null} if null String input
     */
    public static String stripEnd(final String str, final String stripChars) {
        int end = length(str);
        if (end == 0) {
            return str;
        }

        if (stripChars == null) {
            while (end != 0 && Character.isWhitespace(str.charAt(end - 1))) {
                end--;
            }
        } else if (stripChars.isEmpty()) {
            return str;
        } else {
            while (end != 0 && stripChars.indexOf(str.charAt(end - 1)) != INDEX_NOT_FOUND) {
                end--;
            }
        }
        return str.substring(0, end);
    }


    // ContainsAny
    //-----------------------------------------------------------------------
    /**
     * <p>Checks if the CharSequence contains any character in the given
     * set of characters.</p>
     *
     * <p>A {@code null} CharSequence will return {@code false}.
     * A {@code null} or zero length search array will return {@code false}.</p>
     *

     * StringUtils.containsAny(null, *)                  = false
     * StringUtils.containsAny("", *)                    = false
     * StringUtils.containsAny(*, null)                  = false
     * StringUtils.containsAny(*, [])                    = false
     * StringUtils.containsAny("zzabyycdxx", ['z', 'a']) = true
     * StringUtils.containsAny("zzabyycdxx", ['b', 'y']) = true
     * StringUtils.containsAny("zzabyycdxx", ['z', 'y']) = true
     * StringUtils.containsAny("aba", ['z'])             = false

     *
     * @param cs  the CharSequence to check, may be null
     * @param searchChars  the chars to search for, may be null
     * @return the {@code true} if any of the chars are found,
     * {@code false} if no match or null input
     * @since 2.4
     * @since 3.0 Changed signature from containsAny(String, char[]) to containsAny(CharSequence, char...)
     */
    public static boolean containsAny(final CharSequence cs, final char... searchChars) {
        if (isEmpty(cs) || ArrayUtils.isEmpty(searchChars)) {
            return false;
        }
        final int csLength = cs.length();
        final int searchLength = searchChars.length;
        final int csLast = csLength - 1;
        final int searchLast = searchLength - 1;
        for (int i = 0; i < csLength; i++) {
            final char ch = cs.charAt(i);
            for (int j = 0; j < searchLength; j++) {
                if (searchChars[j] == ch) {
                    if (Character.isHighSurrogate(ch)) {
                        if (j == searchLast) {
                            // missing low surrogate, fine, like String.indexOf(String)
                            return true;
                        }
                        if (i < csLast && searchChars[j + 1] == cs.charAt(i + 1)) {
                            return true;
                        }
                    } else {
                        // ch is in the Basic Multilingual Plane
                        return true;
                    }
                }
            }
        }
        return false;
    }

    /**
     * <p>Checks if the CharSequence contains any of the CharSequences in the given array.</p>
     *
     * <p>
     * A {@code null} {@code cs} CharSequence will return {@code false}. A {@code null} or zero
     * length search array will return {@code false}.
     * </p>
     *

     * StringUtils.containsAny(null, *)            = false
     * StringUtils.containsAny("", *)              = false
     * StringUtils.containsAny(*, null)            = false
     * StringUtils.containsAny(*, [])              = false
     * StringUtils.containsAny("abcd", "ab", null) = true
     * StringUtils.containsAny("abcd", "ab", "cd") = true
     * StringUtils.containsAny("abc", "d", "abc")  = true

     *
     *
     * @param cs The CharSequence to check, may be null
     * @param searchCharSequences The array of CharSequences to search for, may be null.
     * Individual CharSequences may be null as well.
     * @return {@code true} if any of the search CharSequences are found, {@code false} otherwise
     * @since 3.4
     */
    public static boolean containsAny(final CharSequence cs, final CharSequence... searchCharSequences) {
        if (isEmpty(cs) || ArrayUtils.isEmpty(searchCharSequences)) {
            return false;
        }
        for (final CharSequence searchCharSequence : searchCharSequences) {
            if (contains(cs, searchCharSequence)) {
                return true;
            }
        }
        return false;
    }

    /**
     * <p>Counts how many times the char appears in the given string.</p>
     *
     * <p>A {@code null} or empty ("") String input returns {@code 0}.</p>
     *

     * StringUtils.countMatches(null, *)       = 0
     * StringUtils.countMatches("", *)         = 0
     * StringUtils.countMatches("abba", 0)  = 0
     * StringUtils.countMatches("abba", 'a')   = 2
     * StringUtils.countMatches("abba", 'b')  = 2
     * StringUtils.countMatches("abba", 'x') = 0

     *
     * @param str  the CharSequence to check, may be null
     * @param ch  the char to count
     * @return the number of occurrences, 0 if the CharSequence is {@code null}
     * @since 3.4
     */
    public static int countMatches(final CharSequence str, final char ch) {
        if (isEmpty(str)) {
            return 0;
        }
        int count = 0;
        // We could also call str.toCharArray() for faster look ups but that would generate more garbage.
        for (int i = 0; i < str.length(); i++) {
            if (ch == str.charAt(i)) {
                count++;
            }
        }
        return count;
    }

    // Count matches
    //-----------------------------------------------------------------------
    /**
     * <p>Counts how many times the substring appears in the larger string.</p>
     *
     * <p>A {@code null} or empty ("") String input returns {@code 0}.</p>
     *

     * StringUtils.countMatches(null, *)       = 0
     * StringUtils.countMatches("", *)         = 0
     * StringUtils.countMatches("abba", null)  = 0
     * StringUtils.countMatches("abba", "")    = 0
     * StringUtils.countMatches("abba", "a")   = 2
     * StringUtils.countMatches("abba", "ab")  = 1
     * StringUtils.countMatches("abba", "xxx") = 0

     *
     * @param str  the CharSequence to check, may be null
     * @param sub  the substring to count, may be null
     * @return the number of occurrences, 0 if either CharSequence is {@code null}
     * @since 3.0 Changed signature from countMatches(String, String) to countMatches(CharSequence, CharSequence)
     */
    public static int countMatches(final CharSequence str, final CharSequence sub) {
        if (isEmpty(str) || isEmpty(sub)) {
            return 0;
        }
        int count = 0;
        int idx = 0;
        while ((idx = indexOf(str, sub, idx)) != INDEX_NOT_FOUND) {
            count++;
            idx += sub.length();
        }
        return count;
    }

    /**
     * Used by the indexOf(CharSequence methods) as a green implementation of indexOf.
     *
     * @param cs the {@code CharSequence} to be processed
     * @param searchChar the {@code CharSequence} to be searched for
     * @param start the start index
     * @return the index where the search sequence was found
     */
    static int indexOf(final CharSequence cs, final CharSequence searchChar, final int start) {
        return cs.toString().indexOf(searchChar.toString(), start);
//        if (cs instanceof String && searchChar instanceof String) {
//            // TODO: Do we assume searchChar is usually relatively small;
//            //       If so then calling toString() on it is better than reverting to
//            //       the green implementation in the else block
//            return ((String) cs).indexOf((String) searchChar, start);
//        } else {
//            // TODO: Implement rather than convert to String
//            return cs.toString().indexOf(searchChar.toString(), start);
//        }
    }

    /**
     * 查找指定字符串是否包含指定字符串列表中的任意一个字符串同时串忽略大小写
     *
     * @param cs 指定字符串
     * @param searchCharSequences 需要检查的字符串数组
     * @return 是否包含任意一个字符串
     */
    public static boolean containsAnyIgnoreCase(CharSequence cs, CharSequence... searchCharSequences)
    {
        if (isEmpty(cs) || ObjectUtils.isEmpty(searchCharSequences))
        {
            return false;
        }
        for (CharSequence testStr : searchCharSequences)
        {
            if (containsIgnoreCase(cs, testStr))
            {
                return true;
            }
        }
        return false;
    }

    /**
     * @param string 输入字符串
     * @param searchStrings 查询匹配度字符串
     * @return 是否包含字符串，包含true，不包含false
     */
    public static boolean equalsAnyIgnoreCase(final CharSequence string, final CharSequence...searchStrings) {
        if (ArrayUtils.isNotEmpty(searchStrings)) {
            for (final CharSequence next : searchStrings) {
                if (equalsIgnoreCase(string, next)) {
                    return true;
                }
            }
        }
        return false;
    }


    /**
     * @param str 输入字符串
     * @return 小写的字符串
     */
    public static String toUnderScoreCase(String str)
    {
        if (str == null)
        {
            return null;
        }
        StringBuilder sb = new StringBuilder();
        // 前置字符是否大写
        boolean preCharIsUpperCase = true;
        // 当前字符是否大写
        boolean curreCharIsUpperCase = true;
        // 下一字符是否大写
        boolean nexteCharIsUpperCase = true;
        for (int i = 0; i < str.length(); i++)
        {
            char c = str.charAt(i);
            if (i > 0)
            {
                preCharIsUpperCase = Character.isUpperCase(str.charAt(i - 1));
            }
            else
            {
                preCharIsUpperCase = false;
            }

            curreCharIsUpperCase = Character.isUpperCase(c);

            if (i < (str.length() - 1))
            {
                nexteCharIsUpperCase = Character.isUpperCase(str.charAt(i + 1));
            }

            if (preCharIsUpperCase && curreCharIsUpperCase && !nexteCharIsUpperCase)
            {
                sb.append(C_UNDERLINE);
            }
            else if ((i != 0 && !preCharIsUpperCase) && curreCharIsUpperCase)
            {
                sb.append(C_UNDERLINE);
            }
            sb.append(Character.toLowerCase(c));
        }

        return sb.toString();
    }

    /**
     * 将下划线大写方式命名的字符串转换为驼峰式。如果转换前的下划线大写方式命名的字符串为空，则返回空字符串。 例如：HELLO_WORLD#HelloWorld
     *
     * @param name 转换前的下划线大写方式命名的字符串
     * @return 转换后的驼峰式命名的字符串
     */
    public static String convertToCamelCase(String name)
    {
        StringBuilder result = new StringBuilder();
        // 快速检查
        if (name == null || name.isEmpty())
        {
            // 没必要转换
            return "";
        }
        else if (!name.contains("_"))
        {
            // 不含下划线，仅将首字母大写
            return name.substring(0, 1).toUpperCase() + name.substring(1);
        }
        // 用下划线将原始字符串分割
        String[] camels = name.split("_");
        for (String camel : camels)
        {
            // 跳过原始字符串中开头、结尾的下换线或双重下划线
            if (camel.isEmpty())
            {
                continue;
            }
            // 首字母大写
            result.append(camel.substring(0, 1).toUpperCase());
            result.append(camel.substring(1).toLowerCase());
        }
        return result.toString();
    }


    /**
     * 替换指定字符串的指定区间内字符为"*"
     *
     * @param str 字符串
     * @param startInclude 开始位置（包含）
     * @param endExclude 结束位置（不包含）
     * @return 替换后的字符串
     */
    public static String hide(CharSequence str, int startInclude, int endExclude)
    {
        if (isEmpty(str))
        {
            return EMPTY;
        }
        final int strLength = str.length();
        if (startInclude > strLength)
        {
            return EMPTY;
        }
        if (endExclude > strLength)
        {
            endExclude = strLength;
        }
        if (startInclude > endExclude)
        {
            // 如果起始位置大于结束位置，不替换
            return EMPTY;
        }
        final char[] chars = new char[strLength];
        for (int i = 0; i < strLength; i++)
        {
            if (i >= startInclude && i < endExclude)
            {
                chars[i] = ASTERISK;
            }
            else
            {
                chars[i] = str.charAt(i);
            }
        }
        return new String(chars);
    }
    /**
     * 是否包含字符串
     *
     * @param str 验证字符串
     * @param strs 字符串组
     * @return 包含返回true
     */
    public static boolean inStringIgnoreCase(String str, String... strs)
    {
        if (str != null && strs != null)
        {
            for (String s : strs)
            {
                if (str.equalsIgnoreCase(trim(s)))
                {
                    return true;
                }
            }
        }
        return false;
    }

    /** 驼峰式命名法，例如：user_name  userName
     * @param s 输入字符串
     * @return 驼峰式命令字符串
     */
    public static String toCamelCase(String s)
    {
        if (s == null)
        {
            return null;
        }
        if (s.indexOf(C_UNDERLINE) == -1)
        {
            return s;
        }
        s = s.toLowerCase();
        StringBuilder sb = new StringBuilder(s.length());
        boolean upperCase = false;
        for (int i = 0; i < s.length(); i++)
        {
            char c = s.charAt(i);

            if (c == C_UNDERLINE)
            {
                upperCase = true;
            }
            else if (upperCase)
            {
                sb.append(Character.toUpperCase(c));
                upperCase = false;
            }
            else
            {
                sb.append(c);
            }
        }
        return sb.toString();
    }


    /**
     * <p>Capitalizes a String changing the first character to title case as
     * per {@link Character#toTitleCase(int)}. No other characters are changed.</p>
     *

     * StringUtils.capitalize(null)  = null
     * StringUtils.capitalize("")    = ""
     * StringUtils.capitalize("cat") = "Cat"
     * StringUtils.capitalize("cAt") = "CAt"
     * StringUtils.capitalize("'cat'") = "'cat'"

     *
     * @param str the String to capitalize, may be null
     * @return the capitalized String, {@code null} if null String input
     * @since 2.0
     */
    public static String capitalize(final String str) {
        final int strLen = length(str);
        if (strLen == 0) {
            return str;
        }

        final int firstCodepoint = str.codePointAt(0);
        final int newCodePoint = Character.toTitleCase(firstCodepoint);
        if (firstCodepoint == newCodePoint) {
            // already capitalized
            return str;
        }

        final int newCodePoints[] = new int[strLen]; // cannot be longer than the char array
        int outOffset = 0;
        newCodePoints[outOffset++] = newCodePoint; // copy the first codepoint
        for (int inOffset = Character.charCount(firstCodepoint); inOffset < strLen; ) {
            final int codepoint = str.codePointAt(inOffset);
            newCodePoints[outOffset++] = codepoint; // copy the remaining ones
            inOffset += Character.charCount(codepoint);
        }
        return new String(newCodePoints, 0, outOffset);
    }

    /**
     * <p>
     * Replaces all occurrences of Strings within another String.
     * </p>
     *
     * <p>
     * A {@code null} reference passed to this method is a no-op, or if
     * any "search string" or "string to replace" is null, that replace will be
     * ignored. This will not repeat. For repeating replaces, call the
     * overloaded method.
     * </p>
     *

     *  StringUtils.replaceEach(null, *, *)        = null
     *  StringUtils.replaceEach("", *, *)          = ""
     *  StringUtils.replaceEach("aba", null, null) = "aba"
     *  StringUtils.replaceEach("aba", new String[0], null) = "aba"
     *  StringUtils.replaceEach("aba", null, new String[0]) = "aba"
     *  StringUtils.replaceEach("aba", new String[]{"a"}, null)  = "aba"
     *  StringUtils.replaceEach("aba", new String[]{"a"}, new String[]{""})  = "b"
     *  StringUtils.replaceEach("aba", new String[]{null}, new String[]{"a"})  = "aba"
     *  StringUtils.replaceEach("abcde", new String[]{"ab", "d"}, new String[]{"w", "t"})  = "wcte"
     *  (example of how it does not repeat)
     *  StringUtils.replaceEach("abcde", new String[]{"ab", "d"}, new String[]{"d", "t"})  = "dcte"

     *
     * @param text
     *            text to search and replace in, no-op if null
     * @param searchList
     *            the Strings to search for, no-op if null
     * @param replacementList
     *            the Strings to replace them with, no-op if null
     * @return the text with any replacements processed, {@code null} if
     *         null String input
     * @throws IllegalArgumentException
     *             if the lengths of the arrays are not the same (null is ok,
     *             and/or size 0)
     * @since 2.4
     */
    public static String replaceEach(final String text, final String[] searchList, final String[] replacementList) {
        return replaceEach(text, searchList, replacementList, false, 0);
    }

    /**
     * <p>Uncapitalizes a String, changing the first character to lower case as
     * per {@link Character#toLowerCase(int)}. No other characters are changed.</p>
     *

     * StringUtils.uncapitalize(null)  = null
     * StringUtils.uncapitalize("")    = ""
     * StringUtils.uncapitalize("cat") = "cat"
     * StringUtils.uncapitalize("Cat") = "cat"
     * StringUtils.uncapitalize("CAT") = "cAT"

     *
     * @param str the String to uncapitalize, may be null
     * @return the uncapitalized String, {@code null} if null String input
     * @since 2.0
     */
    public static String uncapitalize(final String str) {
        final int strLen = length(str);
        if (strLen == 0) {
            return str;
        }

        final int firstCodepoint = str.codePointAt(0);
        final int newCodePoint = Character.toLowerCase(firstCodepoint);
        if (firstCodepoint == newCodePoint) {
            // already capitalized
            return str;
        }

        final int newCodePoints[] = new int[strLen]; // cannot be longer than the char array
        int outOffset = 0;
        newCodePoints[outOffset++] = newCodePoint; // copy the first codepoint
        for (int inOffset = Character.charCount(firstCodepoint); inOffset < strLen; ) {
            final int codepoint = str.codePointAt(inOffset);
            newCodePoints[outOffset++] = codepoint; // copy the remaining ones
            inOffset += Character.charCount(codepoint);
        }
        return new String(newCodePoints, 0, outOffset);
    }


    /**
     * <p>
     * Replace all occurrences of Strings within another String.
     * A {@code null} reference passed to this method is a no-op, or if
     * any "search string" or "string to replace" is null, that replace will be
     * ignored.
     * </p>
     *

     *  StringUtils.replaceEach(null, *, *, *, *) = null
     *  StringUtils.replaceEach("", *, *, *, *) = ""
     *  StringUtils.replaceEach("aba", null, null, *, *) = "aba"
     *  StringUtils.replaceEach("aba", new String[0], null, *, *) = "aba"
     *  StringUtils.replaceEach("aba", null, new String[0], *, *) = "aba"
     *  StringUtils.replaceEach("aba", new String[]{"a"}, null, *, *) = "aba"
     *  StringUtils.replaceEach("aba", new String[]{"a"}, new String[]{""}, *, >=0) = "b"
     *  StringUtils.replaceEach("aba", new String[]{null}, new String[]{"a"}, *, >=0) = "aba"
     *  StringUtils.replaceEach("abcde", new String[]{"ab", "d"}, new String[]{"w", "t"}, *, >=0) = "wcte"
     *  (example of how it repeats)
     *  StringUtils.replaceEach("abcde", new String[]{"ab", "d"}, new String[]{"d", "t"}, false, >=0) = "dcte"
     *  StringUtils.replaceEach("abcde", new String[]{"ab", "d"}, new String[]{"d", "t"}, true, >=2) = "tcte"
     *  StringUtils.replaceEach("abcde", new String[]{"ab", "d"}, new String[]{"d", "ab"}, *, *) = IllegalStateException

     *
     * @param text
     *            text to search and replace in, no-op if null
     * @param searchList
     *            the Strings to search for, no-op if null
     * @param replacementList
     *            the Strings to replace them with, no-op if null
     * @param repeat if true, then replace repeatedly
     *       until there are no more possible replacements or timeToLive &lt; 0
     * @param timeToLive
     *            if less than 0 then there is a circular reference and endless
     *            loop
     * @return the text with any replacements processed, {@code null} if
     *         null String input
     * @throws IllegalStateException
     *             if the search is repeating and there is an endless loop due
     *             to outputs of one being inputs to another
     * @throws IllegalArgumentException
     *             if the lengths of the arrays are not the same (null is ok,
     *             and/or size 0)
     * @since 2.4
     */
    private static String replaceEach(
            final String text, final String[] searchList, final String[] replacementList, final boolean repeat, final int timeToLive) {

        // mchyzer Performance note: This creates very few new objects (one major goal)
        // let me know if there are performance requests, we can create a harness to measure

        if (isEmpty(text) || ArrayUtils.isEmpty(searchList) || ArrayUtils.isEmpty(replacementList)) {
            return text;
        }

        // if recursing, this shouldn't be less than 0
        if (timeToLive < 0) {
            throw new IllegalStateException("Aborting to protect against StackOverflowError - " +
                    "output of one loop is the input of another");
        }

        final int searchLength = searchList.length;
        final int replacementLength = replacementList.length;

        // make sure lengths are ok, these need to be equal
        if (searchLength != replacementLength) {
            throw new IllegalArgumentException("Search and Replace array lengths don't match: "
                    + searchLength
                    + " vs "
                    + replacementLength);
        }

        // keep track of which still have matches
        final boolean[] noMoreMatchesForReplIndex = new boolean[searchLength];

        // index on index that the match was found
        int textIndex = -1;
        int replaceIndex = -1;
        int tempIndex = -1;

        // index of replace array that will replace the search string found
        // NOTE: logic duplicated below START
        for (int i = 0; i < searchLength; i++) {
            if (noMoreMatchesForReplIndex[i] || searchList[i] == null ||
                    searchList[i].isEmpty() || replacementList[i] == null) {
                continue;
            }
            tempIndex = text.indexOf(searchList[i]);

            // see if we need to keep searching for this
            if (tempIndex == -1) {
                noMoreMatchesForReplIndex[i] = true;
            } else {
                if (textIndex == -1 || tempIndex < textIndex) {
                    textIndex = tempIndex;
                    replaceIndex = i;
                }
            }
        }
        // NOTE: logic mostly below END

        // no search strings found, we are done
        if (textIndex == -1) {
            return text;
        }

        int start = 0;

        // get a good guess on the size of the result buffer so it doesn't have to double if it goes over a bit
        int increase = 0;

        // count the replacement text elements that are larger than their corresponding text being replaced
        for (int i = 0; i < searchList.length; i++) {
            if (searchList[i] == null || replacementList[i] == null) {
                continue;
            }
            final int greater = replacementList[i].length() - searchList[i].length();
            if (greater > 0) {
                increase += 3 * greater; // assume 3 matches
            }
        }
        // have upper-bound at 20% increase, then let Java take over
        increase = Math.min(increase, text.length() / 5);

        final StringBuilder buf = new StringBuilder(text.length() + increase);

        while (textIndex != -1) {

            for (int i = start; i < textIndex; i++) {
                buf.append(text.charAt(i));
            }
            buf.append(replacementList[replaceIndex]);

            start = textIndex + searchList[replaceIndex].length();

            textIndex = -1;
            replaceIndex = -1;
            tempIndex = -1;
            // find the next earliest match
            // NOTE: logic mostly duplicated above START
            for (int i = 0; i < searchLength; i++) {
                if (noMoreMatchesForReplIndex[i] || searchList[i] == null ||
                        searchList[i].isEmpty() || replacementList[i] == null) {
                    continue;
                }
                tempIndex = text.indexOf(searchList[i], start);

                // see if we need to keep searching for this
                if (tempIndex == -1) {
                    noMoreMatchesForReplIndex[i] = true;
                } else {
                    if (textIndex == -1 || tempIndex < textIndex) {
                        textIndex = tempIndex;
                        replaceIndex = i;
                    }
                }
            }
            // NOTE: logic duplicated above END

        }
        final int textLength = text.length();
        for (int i = start; i < textLength; i++) {
            buf.append(text.charAt(i));
        }
        final String result = buf.toString();
        if (!repeat) {
            return result;
        }

        return replaceEach(result, searchList, replacementList, repeat, timeToLive - 1);
    }

    /**
     * <p>Case in-sensitive find of the first index within a CharSequence.</p>
     *
     * <p>A {@code null} CharSequence will return {@code -1}.
     * A negative start position is treated as zero.
     * An empty ("") search CharSequence always matches.
     * A start position greater than the string length only matches
     * an empty search CharSequence.</p>
     *

     * StringUtils.indexOfIgnoreCase(null, *)          = -1
     * StringUtils.indexOfIgnoreCase(*, null)          = -1
     * StringUtils.indexOfIgnoreCase("", "")           = 0
     * StringUtils.indexOfIgnoreCase("aabaabaa", "a")  = 0
     * StringUtils.indexOfIgnoreCase("aabaabaa", "b")  = 2
     * StringUtils.indexOfIgnoreCase("aabaabaa", "ab") = 1

     *
     * @param str  the CharSequence to check, may be null
     * @param searchStr  the CharSequence to find, may be null
     * @return the first index of the search CharSequence,
     *  -1 if no match or {@code null} string input
     * @since 2.5
     * @since 3.0 Changed signature from indexOfIgnoreCase(String, String) to indexOfIgnoreCase(CharSequence, CharSequence)
     */
    public static int indexOfIgnoreCase(final CharSequence str, final CharSequence searchStr) {
        return indexOfIgnoreCase(str, searchStr, 0);
    }

    /**
     * <p>Case in-sensitive find of the first index within a CharSequence
     * from the specified position.</p>
     *
     * <p>A {@code null} CharSequence will return {@code -1}.
     * A negative start position is treated as zero.
     * An empty ("") search CharSequence always matches.
     * A start position greater than the string length only matches
     * an empty search CharSequence.</p>
     *

     * StringUtils.indexOfIgnoreCase(null, *, *)          = -1
     * StringUtils.indexOfIgnoreCase(*, null, *)          = -1
     * StringUtils.indexOfIgnoreCase("", "", 0)           = 0
     * StringUtils.indexOfIgnoreCase("aabaabaa", "A", 0)  = 0
     * StringUtils.indexOfIgnoreCase("aabaabaa", "B", 0)  = 2
     * StringUtils.indexOfIgnoreCase("aabaabaa", "AB", 0) = 1
     * StringUtils.indexOfIgnoreCase("aabaabaa", "B", 3)  = 5
     * StringUtils.indexOfIgnoreCase("aabaabaa", "B", 9)  = -1
     * StringUtils.indexOfIgnoreCase("aabaabaa", "B", -1) = 2
     * StringUtils.indexOfIgnoreCase("aabaabaa", "", 2)   = 2
     * StringUtils.indexOfIgnoreCase("abc", "", 9)        = -1

     *
     * @param str  the CharSequence to check, may be null
     * @param searchStr  the CharSequence to find, may be null
     * @param startPos  the start position, negative treated as zero
     * @return the first index of the search CharSequence (always &ge; startPos),
     *  -1 if no match or {@code null} string input
     * @since 2.5
     * @since 3.0 Changed signature from indexOfIgnoreCase(String, String, int) to indexOfIgnoreCase(CharSequence, CharSequence, int)
     */
    public static int indexOfIgnoreCase(final CharSequence str, final CharSequence searchStr, int startPos) {
        if (str == null || searchStr == null) {
            return INDEX_NOT_FOUND;
        }
        if (startPos < 0) {
            startPos = 0;
        }
        final int endLimit = str.length() - searchStr.length() + 1;
        if (startPos > endLimit) {
            return INDEX_NOT_FOUND;
        }
        if (searchStr.length() == 0) {
            return startPos;
        }
        for (int i = startPos; i < endLimit; i++) {
            if (regionMatches(str, true, i, searchStr, 0, searchStr.length())) {
                return i;
            }
        }
        return INDEX_NOT_FOUND;
    }

    /**
     * <p>Gets the substring after the first occurrence of a separator.
     * The separator is not returned.</p>
     *
     * <p>A {@code null} string input will return {@code null}.
     * An empty ("") string input will return the empty string.
     * A {@code null} separator will return the empty string if the
     * input string is not {@code null}.</p>
     *
     * <p>If nothing is found, the empty string is returned.</p>
     *

     * StringUtils.substringAfter(null, *)      = null
     * StringUtils.substringAfter("", *)        = ""
     * StringUtils.substringAfter(*, null)      = ""
     * StringUtils.substringAfter("abc", "a")   = "bc"
     * StringUtils.substringAfter("abcba", "b") = "cba"
     * StringUtils.substringAfter("abc", "c")   = ""
     * StringUtils.substringAfter("abc", "d")   = ""
     * StringUtils.substringAfter("abc", "")    = "abc"

     *
     * @param str  the String to get a substring from, may be null
     * @param separator  the String to search for, may be null
     * @return the substring after the first occurrence of the separator,
     *  {@code null} if null String input
     * @since 2.0
     */
    public static String substringAfter(final String str, final String separator) {
        if (isEmpty(str)) {
            return str;
        }
        if (separator == null) {
            return EMPTY;
        }
        final int pos = str.indexOf(separator);
        if (pos == INDEX_NOT_FOUND) {
            return EMPTY;
        }
        return str.substring(pos + separator.length());
    }

    /**
     * 数字左边补齐0，使之达到指定长度。注意，如果数字转换为字符串后，长度大于size，则只保留 最后size个字符。
     *
     * @param num 数字对象
     * @param size 字符串指定长度
     * @return 返回数字的字符串格式，该字符串为指定长度。
     */
    public static final String padl(final Number num, final int size)
    {
        return padl(num.toString(), size, '0');
    }

    /**
     * 字符串左补齐。如果原始字符串s长度大于size，则只保留最后size个字符。
     *
     * @param s 原始字符串
     * @param size 字符串指定长度
     * @param c 用于补齐的字符
     * @return 返回指定长度的字符串，由原字符串左补齐或截取得到。
     */
    public static final String padl(final String s, final int size, final char c)
    {
        final StringBuilder sb = new StringBuilder(size);
        if (s != null)
        {
            final int len = s.length();
            if (s.length() <= size)
            {
                for (int i = size - len; i > 0; i--)
                {
                    sb.append(c);
                }
                sb.append(s);
            }
            else
            {
                return s.substring(len - size, len);
            }
        }
        else
        {
            for (int i = size; i > 0; i--)
            {
                sb.append(c);
            }
        }
        return sb.toString();
    }

    /**
     * 判断给定的collection列表中是否包含数组array 判断给定的数组array中是否包含给定的元素value
     *
     * @param collection 给定的集合
     * @param array 给定的数组
     * @return boolean 结果
     */
    public static boolean containsAny(Collection<String> collection, String... array)
    {
        if (!ObjectUtils.isEmpty(collection) && !ObjectUtils.isEmpty(array)) {
            for (String str : array) {
                if (collection.contains(str)) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * 获取参数不为空值
     *
     * @param value value 输入
     * @param defaultValue 缺省value
     * @param <T> 泛型包装
     * @return value 返回值
     */
    public static <T> T nvl(T value, T defaultValue)
    {
        return value != null ? value : defaultValue;
    }


    /**
     * <p>Returns either the passed in String,
     * or if the String is {@code null}, an empty String ("").</p>
     *

     * StringUtils.defaultString(null)  = ""
     * StringUtils.defaultString("")    = ""
     * StringUtils.defaultString("bat") = "bat"

     *
     * @see String#valueOf(Object)
     * @param str  the String to check, may be null
     * @return the passed in String, or the empty String if it
     *  was {@code null}
     */
    public static String defaultString(final String str) {
        return defaultString(str, EMPTY);
    }

    /**
     * <p>Returns either the passed in String,
     * or if the String is {@code null}, an empty String ("").</p>
     * @param str  the String to check, may be null
     * @param defaultStr  the default String ""
     * @return the passed in String, or the empty String if it
     *  was {@code null}
     */
    public static String defaultString(final String str, final String defaultStr) {
        return str == null ? defaultStr : str;
    }

    /**
     * 指定字符串是否被包装
     *
     * @param str        字符串
     * @param prefixChar 前缀
     * @param suffixChar 后缀
     * @return 是否被包装
     */
    public static boolean isWrap(CharSequence str, char prefixChar, char suffixChar) {
        if (null == str) {
            return false;
        }

        return str.charAt(0) == prefixChar && str.charAt(str.length() - 1) == suffixChar;
    }

}

